package cn.sccl.common.mybatis;


import cn.sccl.common.model.BaseModel;
import cn.sccl.common.model.PageQueryData;
import cn.sccl.common.util.BeanUtils;
import cn.sccl.common.util.DataBaseUtils;
import cn.sccl.common.model.Page;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.ExecutorException;
import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.BaseStatementHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.scripting.xmltags.ForEachSqlNode;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;

import javax.xml.bind.PropertyException;
import java.lang.reflect.InvocationTargetException;
import java.sql.*;
import java.util.List;
import java.util.Properties;

/**
 * 分页查询插件
 * Created by 杨浩宇 on 2016-02-15.
 */
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class}),
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class PageQueryPlugin implements org.apache.ibatis.plugin.Interceptor {
    private static String pageSqlId = ""; //mapper.xml中需要拦截的ID(正则匹配)

    private static String dialect = "";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (invocation.getTarget() instanceof RoutingStatementHandler) {
            return interceptStatementHandler(invocation);
        } else if (invocation.getTarget() instanceof ResultSetHandler) {
            return interceptResultSetHandler(invocation);
        } else {
            return invocation.proceed();
        }
    }

    /**
     * 拦截StatementHandler，处理count查询，并修改原始sql为分页查询sql
     *
     * @param invocation
     * @return
     */
    private Object interceptStatementHandler(Invocation invocation) throws SQLException, NoSuchFieldException, IllegalAccessException, InvocationTargetException {
        RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation.getTarget();
        BaseStatementHandler delegate = (BaseStatementHandler) BeanUtils.getValue(statementHandler, "delegate");
        MappedStatement mappedStatement = (MappedStatement) BeanUtils.getValue(delegate, "mappedStatement");
        BoundSql boundSql = delegate.getBoundSql();

        if (mappedStatement.getId().matches(pageSqlId)) {
            Object parameterObject = boundSql.getParameterObject();
            if (parameterObject == null) {
                throw new NullPointerException("分页查询时必须传入查询参数！");
            }
            if (parameterObject instanceof BaseModel) {//采用实体类做查询参数
                BaseModel baseModel = (BaseModel) parameterObject;
                if (baseModel.getPage() == null) {//默认设置分页参数
                    baseModel.setPage(Page.newInstance(1, Long.MAX_VALUE));
                }
                //查询总记录数
                long count = this.countQuery((Connection) invocation.getArgs()[0], mappedStatement, boundSql, parameterObject);
                baseModel.getPage().setTotal(count);

                //修改原始SQL为分页查询语句
                String pageSql = DataBaseUtils.getPagedSQL(boundSql.getSql(), baseModel.getPage().getPageIndex(), baseModel.getPage().getPageSize()); //generatePageSql(boundSql.getSql(), baseModel.getPage().getStart() , baseModel.getPage().getPageSize());
                BeanUtils.setFieldValue(boundSql, "sql", pageSql); //将分页sql语句反射回BoundSql.
            } else if (parameterObject instanceof PageQueryData) { //查询参数是PageQueryData对象，进行分页查询
                //查询总记录数
                long count = this.countQuery((Connection) invocation.getArgs()[0], mappedStatement, boundSql, parameterObject);
                PageQueryData pageQueryData = (PageQueryData) parameterObject;

                //修改原始SQL为分页查询语句
                pageQueryData.setResultTotal(count);
                String pageSql = DataBaseUtils.getPagedSQL(boundSql.getSql(), pageQueryData.getPageIndex(), pageQueryData.getPageSize());//generatePageSql(boundSql.getSql() ,pageQueryData.getStart() , pageQueryData.getPageSize());
                BeanUtils.setFieldValue(boundSql, "sql", pageSql); //将分页sql语句反射回BoundSql.
            } else {
                throw new IllegalArgumentException("分页查询时的参数必须传入BaseModel的子类或PageQueryData类");
            }
        }

        return invocation.proceed();
    }

    /**
     * 查询总记录数
     *
     * @param connection
     * @param mappedStatement
     * @param boundSql
     * @param parameterObject
     * @return
     * @throws SQLException
     */
    private long countQuery(Connection connection, MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject) throws SQLException {
        //进行统计查询
        String sql = boundSql.getSql();
        String countSql = DataBaseUtils.getCountSQL(sql); //记录统计 == oracle 加 as 报错(SQL command not properly ended)
        PreparedStatement countStmt = connection.prepareStatement(countSql);
        BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject);
        setParameters(countStmt, mappedStatement, countBS, parameterObject);
        ResultSet rs = countStmt.executeQuery();
        int count = 0;
        if (rs.next()) {
            count = rs.getInt(1);
        }
        rs.close();
        countStmt.close();
        return count;
    }

    /**
     * 拦截ResultSetHandler，处理分页查询的结果，返回Page<E>对象
     *
     * @param invocation
     * @return
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    private Object interceptResultSetHandler(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
        Object resultSet = invocation.proceed();
        DefaultResultSetHandler defaultResultSetHandler = (DefaultResultSetHandler) invocation.getTarget();
        MappedStatement mappedStatement = (MappedStatement) BeanUtils.getValue(defaultResultSetHandler, "mappedStatement");
        BoundSql boundSql = (BoundSql) BeanUtils.getValue(defaultResultSetHandler, "boundSql");

        if (mappedStatement.getId().matches(pageSqlId)) {
            Object parameterObject = boundSql.getParameterObject();
            if (parameterObject == null) {
                throw new NullPointerException("分页查询时必须传入查询参数！");
            }
            if (parameterObject instanceof BaseModel) {
                BaseModel baseModel = (BaseModel) parameterObject;
                if (baseModel.getPage() != null) {
                    Page resultPage = baseModel.getPage().clone();
                    resultPage.setTotal(baseModel.getPage().getTotal());
                    resultPage.addAll((List) resultSet);//结果直接增加进去
                    baseModel.setPage(null);
                    return resultPage;
                }
            } else if (parameterObject instanceof PageQueryData) {//拦截需要分页的SQL
                PageQueryData pageQueryData = (PageQueryData) parameterObject;
                Page resultPage = Page.newInstance(pageQueryData.getPageIndex(), pageQueryData.getPageSize());
                resultPage.setTotal(pageQueryData.getResultTotal());
                resultPage.addAll((List) resultSet);//结果直接增加进去
                return resultPage;
            } else {
                throw new IllegalArgumentException("分页查询时的参数必须传入BaseModel的子类或PageQueryData类");
            }
        }
        //此处取得的parameterObject中的Page对象已经在interceptStatementHandler方法中完成了分页统计查询

        return resultSet;
    }


    /**
     * 对SQL参数(?)设值,参考org.apache.ibatis.executor.parameter.DefaultParameterHandler
     *
     * @param ps
     * @param mappedStatement
     * @param boundSql
     * @param parameterObject
     * @throws SQLException
     */
    private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject) throws SQLException {
        ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
            Configuration configuration = mappedStatement.getConfiguration();
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject);
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    PropertyTokenizer prop = new PropertyTokenizer(propertyName);
                    if (parameterObject == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX) && boundSql.hasAdditionalParameter(prop.getName())) {
                        value = boundSql.getAdditionalParameter(prop.getName());
                        if (value != null) {
                            value = configuration.newMetaObject(value).getValue(propertyName.substring(prop.getName().length()));
                        }
                    } else {
                        value = metaObject == null ? null : metaObject.getValue(propertyName);
                    }
                    TypeHandler typeHandler = parameterMapping.getTypeHandler();
                    if (typeHandler == null) {
                        throw new ExecutorException("There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId());
                    }
                    typeHandler.setParameter(ps, i + 1, value, parameterMapping.getJdbcType());
                }
            }
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        pageSqlId = properties.getProperty("pageSqlId");
        if (StringUtils.isBlank(pageSqlId)) {
            try {
                throw new PropertyException("pageSqlId property is not found!");
            } catch (PropertyException e) {
                e.printStackTrace();
            }
        }
    }
}
